/**
 * @fileOverview
 * Javascript tools to load and use GPS data in a web page. It loads tracks, 
 * waypoints and routes in maps using Mapstraction layer, draws altitude profiles
 * using Flotr charting library, reports derived data and allow user interaction 
 * between those objects.<br>
 * {@link http://jstools4gps.googlecode.com}
 * @author Javier Sanchez Portero
 * @version 0.2
 */
// License ////////////////////////////////////////////////////////////////////
// Copyright (C) 2009 Javier Sanchez Portero
//
// Licensed under the Apache License, Versioan 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////

/** 
 * @name GPS
 * @namespace Holds all funcionality related to tools4gps.js library
 */
var GPS = GPS ? GPS : function() {

    // Private scope section 
    
    /**
     * Solve problem of typeof with null and arrays 
     * (http://javascript.crockford.com/remedial.html)
     * @private
     */
    function _typeOf(value) {
        var s = typeof value;
        if (s === 'object') {
            if (value) {
                if (value instanceof Array) {
                    s = 'array';
                }
            } else {
                s = 'null';
            }
        }
        return s;
    }

    /** 
     * Predefined locale settings
     * @see GPS.setI18N
     * @private
     */
    var _locales = {
      uk: {decsep: null,    dist: 'Km',    elev: 'm',    vel: 'Km/h',    time: 'h', heartrate: 'bpm',
        labeldist: 'Dist.', labeltime: 'Time', labelelev: 'Elev.', labelvel: 'Vel.', labelheartrate: 'H.R.',
        distFactor: 1, elevFactor: 1, velFactor: 1, timeFactor: 1, heartrateFactor: 1,
        distDecs: 2, elevDecs: 0, velDecs: 2, heartrateDecs: 0,
        tagbuttonzoomfalse: 'Select and zoom', 
        tagbuttonzoomtrue: 'Reset zoom',
        tagbuttonxaxisdist: 'Show time in \'X\' axis', 
        tagbuttonxaxishour: 'Show distance in \'X\' axis',
        tagbuttonyaxiselev: 'Show velocity in \'Y\' axis', 
        tagbuttonyaxisvel: 'Show elevation in \'Y\' axis',
        xaxistitledist: 'Distance',
        xaxistitletime: 'Time',
        yaxistitleelev: 'Elevation',
        yaxistitlevel: 'Velocity',
        yaxistitleheartrate: 'Heart Rate',
        dateTimeFormat: 'MM/dd/yyyy hh:mm:ss a'
      },
      es: {decsep: ',',    dist: 'Km',    elev: 'm',    vel: 'Km/h',    time: 'h', heartrate: 'ppm',
        labeldist: 'Dist.', labeltime: 'Tiempo', labelelev: 'Alt.', labelvel: 'Vel.', labelheartrate: 'Pulso',
        distFactor: 1, elevFactor: 1, velFactor: 1, timeFactor: 1, heartrateFactor: 1,
        distDecs: 2, elevDecs: 0, velDecs: 2, heartrateDecs: 0,
        tagbuttonzoomfalse: 'Seleccionar y ampliar', 
        tagbuttonzoomtrue: 'Cancela ampliación',
        tagbuttonxaxisdist: 'Muestra el tiempo en el eje \'X\'', 
        tagbuttonxaxishour: 'Muestra la distancia en el eje \'X\'',
        tagbuttonyaxiselev: 'Muestra la velocidad en el eje \'Y\'', 
        tagbuttonyaxisvel: 'Muestra la altitud en el eje \'Y\'',
        xaxistitledist: 'Distancia',
        xaxistitletime: 'Tiempo',
        yaxistitleelev: 'Altitud',
        yaxistitlevel: 'Velocidad',
        yaxistitleheartrate: 'Pulsaciones',
        dateTimeFormat: 'dd/MM/yyyy HH:mm:ss'
      }
    };

    /** 
     * Default locale setting
     * @private
     */
    var i18n = _locales.uk;

    /** 
     * Create a _MedianFilter object
     * @class Class to filter an array with a median convolution
     * @param {uint} radius Radius of the filter
     * @private
     * @constructor
     */
    function _MedianFilter(radius) {
        this.size = 2 * radius + 1;
        var data = [];
        var results = [];
        var top = 0;        
        // Sets the filter initial values
        data.push({val:-1e10, order:0});
        for (var i=1; i <= this.size+1; i++) {
            data.push({val:1e10, order:0});
        }
        
        /** 
         * Adds a value to the filter and push the result in a FIFO 
         * @private
         */
        this.push = function(v) {
            var temp;   var o = 0;
            if (top < this.size) {
                top++;
            }
            for (var i=1; i <= top; i++) {
                if (data[i].order == this.size) {
                    data.splice(i,1);
                    data.push({val:1e10, order:0});
                }
                if (data[i].val > v && data[i-1].val <= v) {
                    temp = v;   v = data[i].val;     data[i].val = temp;
                    temp = o;   o = data[i].order;   data[i].order = temp;
                }
                data[i].order++;
            }
            var j = Math.floor((top + 1) / 2);
            var median = data[j].val;
            if (top % 2 !== 0) {
                results.push(median);
            }
        };
        
        /**
         * Returns a value from the FIFO
         * @ private
         */
        this.shift = function() { 
            return results.shift(); 
        };
    }
    
    /**
     * Replaces decimal separator
     * @param {float} v A value
     * @param {uint} dec Fixed decimals
     * @return {string} Formatted value
     * @private
     */
    function _formatNumber(v, dec) {
        if (dec !== undefined) { 
            v = (_typeOf(v) == "string") ? parseFloat(v).toFixed(dec) : v.toFixed(dec);
        }
        return (i18n.decsep ? v.toString().replace('.', i18n.decsep) : v);
    }

    /**
     * Converts an hours value to hh:mm format 
     * @param {float} x Hour with decimals
     * @return {string} Formatted value
     * @private
     */
    function _hourToString(x) {
        var i = Math.floor(x);
        var j = Math.floor((x - i) * 60);
        return i + ':' + (j < 10 ? "0" + j : j);
    }

    /** 
     * Parses RFC 3339 date-time format
     * @param {string} val RFC 3339 datetime string
     * @return {Date} val A date
     * {@link http://www.experts-exchange.com/Programming/Languages/Scripting/JavaScript/Q_21889287.html}
     * @private
     */
    function _parseDate(val) {
        var re = /(-)?(\d{4})-(\d{2})-(\d{2})(T(\d{2}):(\d{2}):(\d{2})(\.\d+)?)?(Z|(([\+-])((\d{2}):(\d{2}))))?/;
        if (!val) return null;
        var d = new Date(val.replace(re,'$3/$4/$2 $6:$7:$8 GMT$12$14$15'));
        return(new Date(d.getTime() + 1000 * new Number(val.replace(re, '0$9'))));
    } 

    /** 
     * Apply a function to each element
     * @param {function} func function to apply
     * @param {[element]|Object} an array of elements or a single element.
     * @private
     */
    function _parseElements(func, elements) {
        if (elements !== undefined) {
            if (_typeOf(elements) == 'array') {
                elements.each(func);
            } 
            else {
                func(elements);
            }
        }
    }

    /**
     * Static method to parse a point in a GPX file. 
     * @param {Object}pt 'wpt', 'rtept' or 'trkpt' element parsed from a GPX document
     * @return {GPS.Point} A GPS Point instance
     * @private
     */
    function _GPXparsePt(pt) {
        var lon = parseFloat(pt['-lon']);
        var lat = parseFloat(pt['-lat']);
        var point = new GPS.Point(lat, lon);
        point.elev = pt.ele ? parseFloat(pt.ele) : null;
        point.time = _parseDate(pt.time);
        point.vel = _parseDate(pt.speed) * 3.6; // from m/s to kmh
        point.name = pt.name || null;
        if (pt.desc) {
            point.html = pt.desc;
            if (pt.cmt && (pt.cmt != pt.desc)) {
                point.html += '<br/>' + pt.cmt;
            }
        }
        else if (pt.cmt) {
            point.html = pt.cmt;
        }
        return point;
    }

    /**
     * Static method to parse the tracks in a GPX file. 
     * @param {GPS.Data} gpsData A GPS data set instance
     * @param {[element]} tracks parsed array of 'trk' elements from a GPX document
     * @private
     */
    function _GPXparseTrk(gpsData, tracks) {
        // Parse a GPX TrackPoint into gpsdata
        function parsePoint(trkpt) {
            var point = _GPXparsePt(trkpt);
            gpsData.addTrackpoint(point);
        }
        // Parse a segment
        function parsePoints(trkseg) {
            gpsData.newSegment();
            _parseElements(parsePoint, trkseg.trkpt);
        }
        // Parse data from diferent segments
        function parseSegments(trk) {
            _parseElements(parsePoints, trk.trkseg);
        }
        // Input
        _parseElements(parseSegments, tracks);
    }

    /**
     * Static method to parse the waypoints in a GPX file. 
     * @param {GPS.Data} gpsData A GPS data set instance
     * @param {[element]} wpts parsed array of 'wpt' elements from a GPX document
     * @private
     */
    function _GPXparseWpt(gpsData, wpts) {
        // Parse a GPX WayPoint into gpsdata
        function parsePoint(wpt) {
            var point = _GPXparsePt(wpt);
            gpsData.addWaypoint(point);
        }
        // Input
        _parseElements(parsePoint, wpts);
    }

    /**
     * Static method to parse the routes in a GPX file. 
     * @param {GPS.Data} gpsData A GPS data set instance
     * @param {[element]} routes array of 'rte' elements from a GPX document
     * @private
     */
    function _GPXparseRte(gpsData, routes) {
        // Parse a GPX Route Point into gpsdata
        function parsePoint(rtept) {
            var point = _GPXparsePt(rtept);
            gpsData.addRoutepoint(point);
        }
        // Parse data from diferent routepoints into gpsdata
        function parseRoute(rte) {
            gpsData.newRoute();
            rte.rtept.each(parsePoint);
        }
        // Input
        _parseElements(parseRoute, routes);
    }

    /**
     * Static method to parse a point in a TCX file. 
     * @param {Object} pt 'Trackpoint' parsed from a TCX document
     * @return {GPS.Point} A GPS Point instance
     * @private
     */
    function _TCXparsePt(pt) {
        var lon = parseFloat(pt.Position.LongitudeDegrees);
        var lat = parseFloat(pt.Position.LatitudeDegrees);
        var point = new GPS.Point(lat, lon);
        point.elev = pt.AltitudeMeters ? parseFloat(pt.AltitudeMeters) : null;
        point.time = _parseDate(pt.Time);
        point.heartrate = pt.HeartRateBpm ? parseFloat(pt.HeartRateBpm.Value) : null;
        return point;
    }

    /**
     * Static method to parse the tracks in a TCX file. 
     * @param {GPS.Data} gpsData A GPS data set instance
     * @param {Object} tcx the 'TrainingCenterDatabase' element from a TCX document
     * @param {Object} options GPS.Parse options
     * @private
     */
    function _TCXparse(gpsData, tcx) {
        // Parse a TCX Trackpoint into gpsdata
        function parsePoint(trkpt) {
            var point = _TCXparsePt(trkpt);
            gpsData.addTrackpoint(point);
        }
        // Parse a TCX track into a segment
        function parseTrack(track) {
            gpsData.newSegment();
            _parseElements(parsePoint, track.Trackpoint);
        }
        // Recursively search for tracks elements
        function searchTracks(tcx) {
            _parseElements(parseTrack, tcx.Track);
            _parseElements(searchTracks, tcx.Activities);
            _parseElements(searchTracks, tcx.Activity);
            _parseElements(searchTracks, tcx.Lap);
            _parseElements(searchTracks, tcx.MultiSportSession);
            _parseElements(searchTracks, tcx.FirstSport);
            _parseElements(searchTracks, tcx.NextSport);
            _parseElements(searchTracks, tcx.Transition);
        }
        // Input
        searchTracks(tcx);
    }

    /**
     * Static method to parse a KML point coordinates
     * @param {string} coordinates in the form lon,lat,elev (elev is optional)
     * @return {GPS.Point} A GPS Point instance
     * @private
     */
    function _KMLparsePt(coordinates) {
        if (!coordinates) return null;
        var re = /([-|+|\d]?\d*[\.[\d]+]?)[\s\t\n\r]*,[\s\t\n\r]*([-|+|\d]?\d*[\.[\d]+]?)([\s\t\n\r]*,[\s\t\n\r]*([-|+|\d]?\d*[\.[\d]+]?))?/;
        var m = re.exec(coordinates);
        if (!m) return null;
        var lon = parseFloat(m[1]);
        var lat = parseFloat(m[2]);
        var point = new GPS.Point(lat, lon);
        if (m[4]) {
            point.elev = parseFloat(m[4]);
        }
        return point;
    }

    /**
     * Static method to parse the waypoints and tracks in a KML file. 
     * @param {GPS.Data} gpsData A GPS data set instance
     * @param {Object} kml the 'Document' element from a KML document
     * @param {Object} options GPS.Parse options
     * @private
     */
    function _KMLparse(gpsData, kml, options) {
        // Parse coordinates from a LineString
        function parseLineString(linestring) {
            gpsData.newSegment();
            var re = /[\s\t\n\r]*,[\s\t\n\r]*/g;
            linestring.coordinates = linestring.coordinates.replace(re, ",");
            re = /[\s\t\n\r]+/;
            var m = linestring.coordinates.split(re);
            for (var i=0; i < m.length; i++) {
                if (m[i]) {
                    var point = _KMLparsePt(m[i]);
                    gpsData.addTrackpoint(point);
                }
            }
        }
        // Parse data from each Placemark
        function parsePlacemark(placemarks) {
            if (placemarks.Point && options.wpt) {
                var point = _KMLparsePt(placemarks.Point.coordinates);
                point.name = placemarks.name || null;
                point.html = placemarks.description || null; 
                gpsData.addWaypoint(point);
            }
            if (options.trk) {
                _parseElements(parseLineString, placemarks.LineString);
            }
        }
        // Recursively parse data from each Folder
        function parseFolder(folders) {
            _parseElements(parsePlacemark, folders.Placemark);
            _parseElements(parseFolder, folders.Folder);
        }
        // Input
        parseFolder(kml);
    }

    /**
     * Adds user values 'opts' to default options 'dest' replacing existing ones.
     * @param {Object} opts User options 
     * @param {Object} dest Destination options
     * @private
     */
    function _setOptions(opts, dest) {
        dest = dest || this.options;
        opts = opts || {};
        for (var property in opts) {
            if (_typeOf(dest[property]) == "object" && opts[property]) {
                Object.extend(dest[property], opts[property]);
            }
            else {
                dest[property] = opts[property];
            }
        }
    }

    // Public scope section
    
    var GPS = {};
    
    GPS.version = '0.2';

    /**
     * Static method for setting the locale value for internationalization. Use it 
     * before any other.
     * @param {string, Object} loc Accepted values are 'uk' (default), 'es' or user definned like this:
     * <pre>{
     *    decsep: null,                          // decimal separator character
     *    dist: 'Km', elev: 'm',                 // profile ticks units
     *    vel: 'Km/h', time: 'h', heartrate: 'bpm',
     *    labeldist: 'Dist.', labeltime: 'Time', // profile tracking labels
     *    labelelev: 'Elev.', labelvel: 'Vel.', 
     *    labelheartrate: 'H.R.', 
     *    distFactor: 1, elevFactor: 1,          // profile, report and planner values are multiplied 
     *    velFactor: 1, timeFactor: 1,           // by this factors for unit conversion
     *    heartrateFactor: 1
     *    distDecs: 2, elevDecs: 0,              // numbers of fixed decimals for this values
     *    velDecs: 2, heartrateDecs: 0,
     *    tagbuttonzoomfalse: 'Select and zoom', // titles of buttons
     *    tagbuttonzoomtrue: 'Reset zoom',
     *    tagbuttonxaxisdist: 'Show time in \'X\' axis', 
     *    tagbuttonxaxishour: 'Show distance in \'X\' axis',
     *    tagbuttonyaxiselev: 'Show velocity in \'Y\' axis',
     *    tagbuttonyaxisvel: 'Show elevation in \'Y\' axis',
     *    xaxistitledist: 'Distance',
     *    xaxistitletime: 'Time',
     *    yaxistitleelev: 'Elevation',
     *    yaxistitlevel: 'Velocity',
     *    yaxistitleheartrate: 'Heart Rate',
     *    dateTimeFormat: 'MM/dd/yyyy hh:mm:ss'  // see http://www.javascripttoolbox.com/lib/date/index.php
     * }</pre>
     * For user defined buttons, the title is defined as 'tagbutton' + buttonName + buttonValue.
     * See {@link GPS.Button#options}
     */
    GPS.setI18N = function(loc) {
        i18n = loc ? (_locales[loc] || Object.extend(i18n, loc)) : _locales.uk;
    };

    /**
     * @class Class that represents a track point, route point or waypoint data.
     * @memberOf GPS
     * @extends <a href='http://www.mapstraction.com/doc/LatLonPoint.html'>LatLonPoint</a>
     * @constructor 
     * @param {float} lat Latitude
     * @param {float} lon Longitude
     */
    GPS.Point = function(lat, lon) {
        /** Latitude 
         * @type float */
        this.lat = lat; 
        /** Longitude again 
         * @type float */
        this.lon = lon; 
        /** Longitude 
         * @type float */
        this.lng = lon; 
        /** Elevation in meters 
         * @type int */
        this.elev = null;
        /** Time (date-hour)
          * @type Date */
        this.time =  null;
        /** Horizontal distance (2D) in Km from the track beginning
         * @type float */
        this.dist = 0;
        /** Spatial distance (3D) in Km from the track beginning
         * @type float */
        this.dist3d = 0;
        /** Hours from track beginning
         * @type float */
        this.hour = null;
        /** Horizontal velocity (2D) in Km/s from previous point 
         * @type float */
        this.vel = null;
        /** Spatial velocity (3D) in Km/s from previous point
         * @type float */
        this.vel3d = null;
        /** Heart Rate in beats per minute
         * @type string */
        this.heartrate = null;
        /** Name of the waypoint 
         * @type string */
        this.name = null;
        /** Description or comment of the waypoint
         * @type string */
        this.html = null;
    };
    GPS.Point.prototype = new LatLonPoint();

    /**
     * Create a GPS.Data object
     * @class Class that represents a GPS set of data (tracks, routes and waypoints).
     * @memberOf GPS
     * @constructor
     */
    GPS.Data = function() {
        /** Array of segments, wich are arrays of Track Points.
          * @type [[GPS.Point]] */
        this.trackpoints = [];
        /** Array of WayPoints 
         * @type [GPS.Points] */
        this.waypoints = [];
        /** Array of Routes, wich are arrays of Route points.
         * @type [[GPS.Points]] */
        this.routepoints = [];
        /** Total number of segments in the tracks
         * @type uint */
        this.numOfSegments = 0;
        /** Total number of points in the tracks
         * @type uint */
        this.numOfTrackpoints = 0;
        /** Total number of waypoints  
         * @type uint */
        this.numOfWaypoints = 0;
        /** Total number of routes  
         * @type uint */
        this.numOfRoutes = 0;
        /** Total number of route points in the routes
         * @type uint */
        this.numOfRoutepoints = 0;
        
        /**
         * Gets the trackpoint/routepoint at a index position as if all segments where joined.
         * @param {uint} n Index of the desired trackpoint.
         * @param {strig} src If 'rte', source is routepointes, else trackpoints.
         * @return {GPS.Point}
         */
        this.pointAt = function(n, src) {
            var tp = src == 'rte' ? this.routepoints : this.trackpoints;
            var i = 0;
            while (n - tp[i].length >= 0) {
                n -= tp[i].length;
                i++;
            }
            return tp[i][n];
        };
        
        /**
         * Inserts a new void segment in trackpoints
         */
        this.newSegment = function() {
            this.trackpoints.push([]);
            this.numOfSegments++;
        };
        
        /**
         * Inserts a new void route in routepoints
         */
        this.newRoute = function() {
            this.routepoints.push([]);
            this.numOfRoutes++;
            lastPoint = null;
        };

        /**
         * Searchs for nearest point in the track given a key-value pair.
         * @param {float} val Value to search for.
         * @param {string} key Name of the field to search in.
         * @return {uint} Index position of the point as if all segments where joined.
         */
        this.indexOf = function(val, key) {
            var key = key || 'dist';
            var low = 0;
            var high = this.numOfTrackpoints - 1;
            while (low <= high) {
                var mid = Math.floor((low + high) / 2);
                if (this.pointAt(mid)[key] === null) {
                    low++;
                }
                else {
                    if (this.pointAt(mid)[key] > val) {
                        high = mid - 1;
                    }
                    else if (this.pointAt(mid)[key] < val) {
                        low = mid + 1;
                    }
                    else {
                        return mid; // found
                    }
                }
             }
             return mid; // not found
        };
        
        /**
         * Get bounds of a track segment.
         * @param {uint} first Index of the first segment point.
         * @param {uint} last Index of the last segment point
         * @return {BoundingBox} A mapstraction 
         * <a href="http://www.mapstraction.com/doc/BoundingBox.html">BoundingBox</a> object.
         */
        this.getBounds = function(first, last) {
            var first = first || 0;
            var last = last || this.numOfTrackPoints;
            var p = this.pointAt(first);
            var swlat = p.lat, nelat = swlat, swlon = p.lon, nelon = swlon;
            for (var i = first + 1; i <= last; i++) {
                p = this.pointAt(i);
                swlat = (p.lat < swlat) ? p.lat : swlat;
                swlon = (p.lon < swlon) ? p.lon : swlon;
                nelat = (p.lat > nelat) ? p.lat : nelat;
                nelon = (p.lon > nelon) ? p.lon : nelon;
            }
            return (new BoundingBox(swlat, swlon, nelat, nelon));
        }

        /**
         * Calculates derived data for a point of a sequence
         * @param {GPS.Point} point Actual point in the sequence
         * @param {GPS.Point} lastPoint Prior point in the sequence
         * @private
         */
        function calcPoint(point, lastPoint) {
            if (lastPoint) { 
                var dist = point.distance(lastPoint);
                point.dist = lastPoint.dist + dist;
                var dist3d = dist;
                if (point.elev !== null && lastPoint.elev !== null) {
                    var delev = (point.elev - lastPoint.elev) / 1000;
                    dist3d = Math.sqrt(Math.pow(dist, 2) + Math.pow(delev, 2));
                }
                point.dist3d = lastPoint.dist3d + dist3d;
                if (point.time !== null && lastPoint.time !== null) {
                    var dt = (point.time - lastPoint.time) / 3600000;
                    var v = 0,  v3d = 0;
                    if (dt !== 0) {
                        v = dist / dt;  
                        v3d = dist3d / dt; 
                    }
                    point.vel = point.vel || v;
                    point.hour = lastPoint.hour + dt;
                    point.vel3d = v3d;
                }
            }
        }
        
        /**
         * Copy derived data and elev from a point to another
         * @param {GPS.Point} to Target
         * @param {GPS.Point} from Source
         * @private
         */
        function copyPoint(to, from) {
            to.dist = from.dist;
            to.dist3d = from.dist3d;
            to.hour = from.hour;
            to.elev = from.elev;
            to.vel = from.vel;
            to.vel3d = from.vel3d;
            to.heartrate = from.heartrate;
        }
        
        /**
         * Returns a point in the tracks if another is at less than delta distance
         * @param {GPS.Point} point A point
         * @param {float} delta Minimun distance to the track
         * @return {GPS.Point} a point or null
         * @private
         */
        function isInTrack(point, delta) {
            var minDis = Number.MAX_VALUE;
            var near = null;
            for (var i=0; i < _this.numOfTrackpoints; i++) {
                var dist = point.distance(_this.pointAt(i));
                if (dist < minDis) {
                    near = _this.pointAt(i);
                    minDis = dist;
                }
            }
            return (minDis <= delta) ? near : null;
        }

        /**
         * Adds a point to the last segment and calculates derived data for it
         * (distance, velocity, hour).
         * @param {GPS.Point} trackpoint Point to be added.
         */
        this.addTrackpoint = function(trackpoint) {
            if (this.numOfSegments === 0) {
                this.newSegment();
            }
            calcPoint(trackpoint, lastPoint);
            this.trackpoints[this.numOfSegments - 1].push(trackpoint);
            this.numOfTrackpoints++;
            lastPoint = trackpoint;
        };

        /**
         * Adds a point to the list of waypoints.
         * @param {GPS.Point} waypoint Point to be added.
         */
        this.addWaypoint = function(waypoint) {
            this.waypoints.push(waypoint);
            this.numOfWaypoints++;
            lastPoint = waypoint;
        };

        /**
         * Adds a point to the last route and calculates derived data for it
         * (distance, velocity, hour).
         * @param {GPS.Point} waypoint Point to be added.
         */
        this.addRoutepoint = function(routepoint) {
            if (this.numOfRoutes === 0) {
                this.newRoute();
            }
            calcPoint(routepoint, lastPoint);
            this.routepoints[this.numOfRoutes - 1].push(routepoint);
            this.numOfRoutepoints++;
            lastPoint = routepoint;
        };

        /**
         * Adjust waypoints to the point nearest than this distance (Km) in the track.
         * @param {float} Minimum distance to the track.
         */
        this.adjustWayPointsToTrack = function(distance) {
            for (var i=0; i < this.numOfWaypoints; i++) {
                var near = isInTrack(this.waypoints[i], distance);
                if (near) {
                    copyPoint(this.waypoints[i], near);
                }
                else {
                    this.waypoints[i].dist = null;
                    this.waypoints[i].dist3d = null
                    this.waypoints[i].hour = null;
                }
            }
        }

        /**
         * Adjust routepoints to the point nearest than this distance (Km) in the track.
         * @param {float} Minimum distance to the track.
         */
        this.adjustRoutePointsToTrack = function(distance) {
            for (var i=0; i < this.numOfRoutes; i++) {
                for (var j=0; j < this.routepoints[i].length; j++) {
                    var near = isInTrack(this.routepoints[i][j], distance);
                    if (near) {
                        copyPoint(this.routepoints[i][j], near);
                    }
                }
            }
        }
        
        // Constructor
        var lastPoint = null;
        var _this = this;
    };

    /**
     * Create a GPS.Summary object
     * @param {GPS.Data} data Object that contents the track
     * @param {Object} opts User defined options (see {@link GPS.Summary#options})
     * @class Class that represents a set of global properties derived from a track
     */
    GPS.Summary = function(data, opts) {
        /** GPS data set
         * @type GPS.Data */
        this.data = data;
        /** 
         * User defined options.
         * Default values:
         * <pre>{source: 'trk',    // 'rte' if there isn'tracks
         *     filterRadius: 2, stoppedDist: 0.001, stoppedVel: 0.5, minSlope: 0.05 }</pre>
         * Properties:
         * <pre>{string} source - 'trk' for tracks or 'rte' for routes.
         * {uint} filterRadius - In order to avoid GPS acquisition noise, elevation is 
         *     median filtered with it 'filterRadius' neighbours before it calculates 
         *     elevation gain/loss. 0 = no filter.
         * {float} stoppedDist - Movements lower than this spatial distance (in meters) are ignored.
         * {float} stoppedVel  - Movements lower than this spatial velocity (in Km/h) are ignored.
         * {float} minSlope    - Below this slope (percent) is considered as flat.</pre>
         * @type Object
         */
        this.options = {
            filterRadius: 2,
            stoppedDist: 1/1000,
            stoppedVel: 0.5,
            minSlope: 0.05
        };

        /**
         * Adds user values to default options replacing existing ones.
         * @param {Object} opts User options 
         * @see GPS.Summary#options
         */
        this.setOptions = function(opts) {
            _setOptions(opts, this.options);
        };

        /**
         * Sets derived data to it initial values (0 or not calcuated)
         */
        this.reset = function() {
            /** Total horizontal (2D) distance in Km 
             * @type float */
            this.totDist = 0;
            /** Total spatial distance (3D) in Km 
             * @type float */
            this.totDist3d = 0;
            /** Minimum elevation in meters 
             * @type float */
            this.minElev = null;
            /** Maximum elevation in meters
             * @type float */
            this.maxElev = null;
            /** Elevation Gain in meters 
             * @type float */
            this.elevGain = null;
            /** Elevation Loss in meters
             * @type float */
            this.elevLoss = null;
            /** Minimum heart rate in beats per minute
             * @type float */
            this.minHeartrate = null;
            /** Maximum heart rate in beats per minute
             * @type float */
            this.maxHeartrate = null;
            /** Average heart rate in beats per minute
             * @type float */
            this.avgHeartrate = null;
            /** Maximum velocity in Km/s
             * @type float */
            this.maxVel = null;
            /** Average velocity in Km/s
             * @type float */
            this.avgVel = null;
            /** Total time in hours 
             * @type float */
            this.totTime = null;
            /** Total time stopped in hours 
             * @type float */
            this.totTimeStop = null;
            /** Total time going up in hours
             * @type float */
            this.totTimeUp = null;
            /** Total time going down in hours 
             * @type float */
            this.totTimeDown = null;
            /** Total time going in flat in hours 
             * @type float */
            this.totTimeFlat = null;
            /** Total time in movement in hours 
             * @type float */
            this.totTimeMove = null;
        };

        /**
         * Calculates acummulated data for a range of the track/route.
         * All tracks and segments/routes are considered as a unique joined track/route.
         * @param {uint} from Index of the range start point.
         * @param {uint} to Index of the range end point.
         */
        this.calculate = function(from, to) {
            var src = this.options.source;
            var fradius = this.options.filterRadius;
            if ((this.data.numOfTrackpoints < fradius) && (src != 'rte')) {
                return;
            }
            var filter = new _MedianFilter(fradius);
            this.reset();
            var from = from || 0;
            var to = (to === undefined ? ((src == 'rte' ? this.data.numOfRoutepoints : this.data.numOfTrackpoints) - 1) : to);
            var totv = 0, numv = 0, tothr = 0, numhr = 0;
            var lastPoint = this.data.pointAt(from, src);
            var lastelev = lastPoint.elev;
            var i = 0;
            if (src != 'rte') {
                for (i = 0; i <= fradius; i++) {
                    filter.push(this.data.pointAt(from+i).elev);
                }
            }
            filter.shift();
            // for each point
            for (i = from + 1; i <= to; i++) {
                var point = this.data.pointAt(i, src);
                var delev = 0, ddist = point.dist - lastPoint.dist;
                this.totDist += ddist;
                this.totDist3d += (point.dist3d - lastPoint.dist3d);
                var elev = point.elev || 0;
                if (point.elev !== null) {
                    // Elevation is median filtered before it calculates elevation gain/loss
                    if ((i < to - fradius + 1) && (src != 'rte')) {
                        filter.push(this.data.pointAt(i+fradius).elev);
                        elev = filter.shift();
                    }
                    if ((this.maxElev === null) || (elev > this.maxElev)) {
                        this.maxElev = elev;
                    }
                    if ((this.minElev === null) || (elev < this.minElev)) {
                        this.minElev = elev;
                    }
                    delev = elev - lastelev;
                    this.elevGain += ((delev > 0) ? delev : 0);
                    this.elevLoss += ((delev < 0) ? delev : 0);
                }
                if (point.time !== null) {
                    this.totTime += (point.hour - lastPoint.hour);
                    var dt = point.hour - lastPoint.hour;
                    // Points are not considered when there isn't movement
                    var stopped = (point.dist3d - lastPoint.dist3d < this.options.stoppedDist);
                    stopped = stopped || (point.vel3d < this.options.stoppedVel);
                    if (stopped) {
                        this.totTimeStop += dt;
                    }
                    else {
                        this.totTimeMove += dt;
                        // Calculates slope to distinguish flat values
                        var slope = Math.abs(delev / ddist / 1000.0 );
                        slope /= (point.dist - lastPoint.dist);
                        if (slope < this.options.minSlope) {
                            delev = 0;
                        }
                        if (delev > 0) {
                            this.totTimeUp += dt;
                        }
                        else if (delev < 0) {
                            this.totTimeDown += dt;
                        }
                        else {
                            this.totTimeFlat += dt;
                        }
                        if (point.vel !== null) {
                            totv += point.vel;
                            numv += 1;
                            if ((this.maxVel === null) || (point.vel > this.maxVel)) {
                                this.maxVel = point.vel;
                            }
                        }
                    }
                }
                if (point.heartrate !== null) {
                    tothr += point.heartrate;
                    numhr += 1;
                    if ((this.maxHeartrate === null) || (point.heartrate > this.maxHeartrate)) {
                        this.maxHeartrate = point.heartrate;
                    }
                    if ((this.minHeartrate === null) || (point.heartrate < this.minHeartrate)) {
                        this.minHeartrate = point.heartrate;
                    }
                }
                lastPoint = point;
                lastelev = elev;
            }
            this.avgVel = numv ? totv / numv : null;
            this.avgHeartrate = numhr ? tothr / numhr : null;
        };

        // Constructor
        this.options.source = 
            ((data.numOfTrackpoints == 0) && (data.numOfRoutepoints > 0)) ? 'rte' : 'trk';
        this.setOptions(opts);
    };

    /**
     * Create a GPS.Parser object
     * <br>Examples: http://jstools4gps.javiersanp.com
     * @param {string} url Source of data
     * @param {Object} opts User defined options (see {@link GPS.Parser#options})
     * @class Class to parse a source of GPS data into a GPS.Data object.
     * Available source formats are GPX (tracks, waypoints and routes), 
     * Google Earth KML (tracks and waypoints) and Garmin TCX (tracks).
     */
    GPS.Parser = function(url, opts) {
        this.url = url;    
        /** 
         * User defined options. Default values:
         * <pre>{ method: 'post', trk: true, wpt: true, rte: true, adjustPoints: 0.050 }</pre>
         * Properties:
         * <pre>{string} method - 'post' or 'get'  // Request method
         * {boolean} trk - If false, ignores tracks
         * {boolean} wpt - If false, ignores waypoints
         * {boolean} rte - If false, ignores routes
         * {float} adjustPoints - If not 0, adjust waypoints and routepoints to the point 
         * nearest than this distance (Km) in the track.</pre>
         * @type Object
         */
        this.options = {
            method: 'post', trk: true, wpt: true, rte: true, adjustPoints: 0.050
        };

        /**
         * Adds user values to default options replacing existing ones. See 
         * @param {Object} opts User options 
         * @see GPS.Parser#options
         */
        this.setOptions = function(opts) {
            _setOptions(opts, this.options);
        };

        /**
         * Does an asynchronous ajax request for data. On success, parses 
         * them and calls to onSuccessAction.
         * @param {function (data)} onSuccessAction User definned function that 
         * runs after successfull parsing. A {GPS.Data} data parameter with the
         * parsed data is passed to this function.
         */
        this.run = function(onSuccessAction) {
            new Ajax.Request(this.url, {
                method: _this.options.method,
                onSuccess: function(request) {  
                    var xotree = new XML.ObjTree();
                    var json = xotree.parseXML( request.responseText );
                    if (json.parsererror) {
                        alert(json.parsererror['#text']);
                    }
                    else if (json.gpx) {
                        if (_this.options.trk && json.gpx.trk) {
                            _GPXparseTrk(gpsData, json.gpx.trk);
                        }
                        if (_this.options.wpt && json.gpx.wpt) {
                            _GPXparseWpt(gpsData, json.gpx.wpt);
                        }
                        if (_this.options.rte && json.gpx.rte) {
                            _GPXparseRte(gpsData, json.gpx.rte);
                        }
                    }
                    else if (json.kml) {
                        _KMLparse(gpsData, json.kml.Document ? json.kml.Document : json.kml, _this.options);
                    }
                    else if (json.TrainingCenterDatabase) {
                        if (_this.options.trk) {
                            _TCXparse(gpsData, json.TrainingCenterDatabase);
                        }
                    }
                    if (_this.options.adjustPoints && _this.options.trk) {
                        if (_this.options.wpt) {
                            gpsData.adjustWayPointsToTrack(_this.options.adjustPoints);
                        }
                        if (_this.options.rte) {
                            gpsData.adjustRoutePointsToTrack(_this.options.adjustPoints);
                        }
                    }
                    if (onSuccessAction) {
                        document.observe('dom:loaded', onSuccessAction(gpsData));
                    }
                },
                onException: function(request, exception) { 
                    alert("Exception " + exception.name + ": " + exception.message); 
                }
            });
        };
        // Constructor
        var _this = this;
        var gpsData = new GPS.Data();
        this.setOptions(opts);
    };
    
    /**
     * Create a GPS.Map object 
     * <br>Examples: http://jstools4gps.javiersanp.com
     * @param {Mapstraction} map A Mapstraction map object
     * @param {GPS.Data} gpsData GPS.Data object to be loaded in the map
     * @param {Object} opts User defined options (see {@link GPS.Map#options}
     * @class Class to show GPS data in a map and control related events
     */
    GPS.Map = function(map, gpsData, opts) {
        /** Mapstraction map object
         * @type Mapstraction.map */
        this.map = map;
        /** GPS data set
         * @type GPS.Data */
        this.gpsData = gpsData;
        /** 
         * User defined options. Default values:
         * <pre>{
         *     trk: {width: 3, color: '#ff0000', opacity: 1},
         *     rte: {width: 3, color: '#00ff00', opacity: 1},
         *     wpt: {},
         *     join: true, 
         *     info: true,
         *     chart: false,
         *     marker: {
         *         icon: "img/icon52.png", iconSize: [32,32], iconAnchor: [16,16], 
         *         iconShadow: "img/void.png", iconShadowSize: [32,32]
         *     },
         *     followMarker: true
         * }</pre>
         * Properties:
         * <pre>{boolean | object} trk - If false, don't draw tracks. If object, set
         *     how to draw the tracks using mapstraction <a href='http://www.mapstraction.com/doc/Polyline.html'>Polyline</a> options:
         *     {uint} width - Track/route width
         *     {string} color - Track/route color in the form #RRGGBB
         *     {float} opacity - Track/route opacity between 0.0 and 1.0
         * {boolean | object} rte - If false, don't draw routes. If object, set
         * how to draw the routes using mapstraction Polyline options (see trk).
         * {boolean | object} wpt - If false, don't draw waypoints. If object, set
         *     how to draw the waypoints using mapstraction <a href='http://www.mapstraction.com/doc/Marker.html'>Marker</a> options:
         *     {string} icon - Url of the image to be used as icon
         *     {[uint, uint]} iconSize - Array size in pixels of the icon image
         *     {[uint, uint]} iconAnchor - Array offset of the anchor point
         *     {string} iconShadow - Url of the image to be used as shadow of the icon
         *     {[uint, uint]} iconShadowSize - Array size in pixels of the shadow image
         * {boolean} join - If true, join all the segments, tracks or routes
         * {boolean | string} info - Set how is showed the waypoints info. None 
         *     if false, in a bubble popup if true or in a DOM element identified by it id.
         * {GPS.Profile} chart - A Profile object to link with (a marker in the map 
         *     follows mouse movement in the profile) or false
         * {Object} marker - Set the options of the marker linked to a chart
         *     using mapstraction Marker options (see wpt). 
         * {boolean}followMarker - If true, the maps pans to the point 
         *     corresponding the mouse movement in the profile.</pre>
         * @type Object
         */
        this.options = {
            trk: {width: 3, color: '#ff0000', opacity: 1}, 
            rte: {width: 3, color: '#00ff00', opacity: 1}, 
            wpt: {}, 
            marker: {
                icon: "img/icon52.png", iconSize: [32,32], iconAnchor: [16,16], 
                iconShadow: "img/void.png", iconShadowSize: [32,32]
            },
            join: true, info: true, chart: false, followMarker: true
        };

        /**
         * Adds user values to default options replacing existing ones.
         * @param {Object} opts User options 
         * @see GPS.Map#options
         */
        this.setOptions = function(opts) {
            _setOptions(opts, this.options);
        };

        /**
         * Shows tracks, routes and waypoints in the map.
         * @param {Object} opts User defined options
         * @see GPS.Map#options
         */
        this.draw = function(opts) {
            var o = Object.clone(this.options);
            Object.extend(o, opts || {});
            var i = 0, j = 0;
            var tp = [[]];
            var myPoly = null;
            if (o.trk) {
                if (o.join) {
                    for (i=0; i < this.gpsData.numOfSegments; i++) {
                        for (j=0; j < this.gpsData.trackpoints[i].length; j++) {
                            tp[0].push(this.gpsData.trackpoints[i][j]);
                        }
                    }
                }
                else {
                    tp = this.gpsData.trackpoints;
                }
                for (i=0; i < tp.length; i++) {
                    if (tp[i].length) {
                        myPoly = new Polyline(tp[i]);
                        this.map.addPolylineWithData(myPoly, o.trk);
                    }
                }
            }
            tp = [[]];
            if (o.rte) {
                if (o.join) {
                    for (i=0; i < this.gpsData.numOfRoutes; i++) {
                        for (j=0; j < this.gpsData.routepoints[i].length; j++) {
                            tp[0].push(this.gpsData.routepoints[i][j]);
                        }
                    }
                }
                else {
                    tp = this.gpsData.routepoints;
                }
                for (i=0; i < tp.length; i++) {
                    if (tp[i].length) {
                        myPoly = new Polyline(tp[i]);
                        this.map.addPolylineWithData(myPoly, o.rte);
                    }
                }
            }
            if (o.wpt) {
                for (i=0; i < this.gpsData.numOfWaypoints; i++) {
                    var p = this.gpsData.waypoints[i];
                    var marker = new Marker(p);
                    if (p.name) {
                        marker.setLabel(p.name);
                    }
                    if (p.html) {
                        if (_typeOf(o.info) == "string") {
                            marker.setInfoDiv(p.html, o.info);
                        }
                        else if (o.info) {
                            marker.setInfoBubble(p.html);
                        }
                    }
                    this.map.addMarkerWithData(marker, o.wpt);
                }
            this.map.autoCenterAndZoom(); 
            }
        };

        // Constructor
        var _this = this;
        var marker;
        this.setOptions(opts);
        var chart = this.options.chart;
        // Hooks to the chart profile mousemove event to redraw a marker in the map or pan it.
        if (chart) {
            chart.container.observe('flotr:mousemove', function(event) {
                var position = event.memo[1];
                var i = _this.gpsData.indexOf(position.x, _this.options.chart.options.x);
                if (marker) {
                    _this.map.removeMarker(marker);
                }
                marker = new Marker(_this.gpsData.pointAt(i));
                _this.map.addMarkerWithData(marker, _this.options.marker);
                if (_this.options.followMarker) {
                    // If the api is google, pan looks better than center (the track don't wanish).
                    if (_this.map.api == 'google') {
                        _this.map.getMap().panTo(marker.location.toGoogle());
                    }
                    else {
                        _this.map.setCenter(marker.location);
                    }
                }
            });
            // Hooks to the chart select event for map redrawing on zoom
            chart.container.observe('flotr:select', function(evt) {
                var area = evt.memo[0];
                var i = gpsData.indexOf(area.x1, _this.options.chart.options.x);
                var j = gpsData.indexOf(area.x2, _this.options.chart.options.x);
                _this.map.setBounds(gpsData.getBounds(i, j));
            });
            // Hooks to the chart mapautozoom event to reset zoom map
            chart.container.observe('tools4gps:mapautozoom', function() {
                _this.map.autoCenterAndZoom();
            });
        }
        this.draw();
        return this;
    };

    /**
     * Create a GPS.Profile object
     * <br>Examples: http://jstools4gps.javiersanp.com
     * @param {string} idprofile Id of the profile DOM element
     * @param {GPS.Data} gpsData Source of data
     * @param {para: value...} opts User defined options. See {@link GPS.Profile#options}
     * @class Class to show diferent profiles (elevation, velocity) in a
     * <a href='http://solutoire.com/flotr'>Flotr</a> chart.
     */
    GPS.Profile = function(idprofile, gpsData, opts) {
        /** Id of the DOM node than contains the Flotr chart 
         * @type node */
        this.container = $(idprofile);
        /** GPS data set
         * @type GPS.Data */
        this.gpsData = gpsData;
        /** 
         * User defined options. Default values:
         * <pre>{ 
         *     x: 'dist',     y: 'elev', 
         *     colors: ['#4da74d', '#00A8F0'],
         *     join: true,    trackingEnabled: true,   zoomEnabled: false,
         *     xFactor: null, yFactor: null, 
         *     xUnit: false,  yUnit: false,
         *     minDeltaX: 20 m or 20 seg, 
         *     trk: true,     wpt: true,               rte: true
         * }</pre>
         * Properties:
         * <pre>{string} x: this.gpsData property to be used in the x axis. Admited 
         *     values are 'dist', 'dist3d' or 'hour'
         * {string} y: this.gpsData property to be used in the y axis. Admited 
         *     values are 'elev', 'vel', 'vel3d' or 'heartRate'
         * {[string]} colors: the first color is the default for distance profiles,
         *     the second for time profiles.
         * {boolean} join: if true, join the diferent segments and tracks
         * {boolean} trackingEnabled: If true, enable mouse tracking 
         *     (shows x-y values in one corner of the chart)
         * {boolean} zoomEnabled: If true, enable mouse selection and zoom
         * {uint} xDecs: Fixed decimals for x values in the trackbox. If null, 
         *     i18n values will be used (see {@link GPS.setI18N})
         * {uint} yDecs: Fixed decimals for y values in the track box. If null, 
         *     i18n values will be used (see {@link GPS.setI18N})
         * {float} xFactor: X values are multiplied by this factor for unit conversion.
         *     If null, i18n factors will be used (see {@link GPS.setI18N})
         * {float} yFactor: Y values are multiplied by this factor for unit conversion. 
         *     If null, i18n factors will be used (see {@link GPS.setI18N})
         * {string} xUnit: Put a string to show in X axis ticks units. If null,
         *     i18n units will be used (see {@link GPS.setI18N})
         * {string} yUnit: Put a string to show in Y axis ticks units. If null, 
         *     i18n units will be used (see {@link GPS.setI18N})
         * {float} minDeltaX: Filter to draw points in the x axis,
         * {boolean | object} trk - If false, don't show tracks, if object, 
         *     they are drawed with these options. Use properties for a plot serie as defined in
         *     <a href='http://solutoire.com/flotr/docs/options/'>Plotr documentation</a>
         * {boolean | object} wpt - If false, don't show waypoints, if object, 
         *     they are drawed with these options. Use properties for a plot serie as defined in
         *     <a href='http://solutoire.com/flotr/docs/options/'>Plotr documentation</a>
         * {boolean | object} rte - If false, don't show routes, if object, 
         *     they are drawed with these options. Use properties for a plot serie as defined in
         *     <a href='http://solutoire.com/flotr/docs/options/'>Plotr documentation</a></pre>
         * @type Object
         */
        this.options = {
            x: 'dist', y: 'elev', 
            colors: ['#4da74d', '#00A8F0'],
            join: true,   trackingEnabled: true,    zoomEnabled: false,
            xDecs: null, yDecs: null,      // Fixed decimals for tracking
            xFactor: null, yFactor: null,  // unit conversion
            xUnit: false, yUnit: false,
            xaxis: {}, yaxis: {},
            HtmlText: false,
            trk: true,    wpt: true,    rte: true,
            mouse: {
                track: true, trackMode: 'x', lineColor: 'purple',
                sensibility: 1, trackFormatter: _trackFormatter},
            selection: {mode: null}
        };
        
        /**
         * Adds user values in opts to dest replacing existing ones.
         * @param {Object} opts User options 
         * @param {Object} dest Destionation options
         * @see GPS.Profile#options
         */
        this.setOptions = function(opts, dest) {
            var o = dest || this.options;
            _setOptions(opts, o);
            o.xaxis.tickFormatter = o.xaxis.tickFormatter || function(n) { 
                return (_this.options.x == 'hour' ? _hourToString(n) : _formatNumber(n)) +
                        ' ' + (_this.options.xUnit || units[_this.options.x]);
            };
            o.yaxis.tickFormatter = o.yaxis.tickFormatter || function(n){ 
                return (_this.options.y == 'hour' ? _hourToString(n) : _formatNumber(n)) +
                    ' ' + (_this.options.yUnit || units[_this.options.y]);
            };
            o.xaxis.title = _this.options.x == 'hour' ? i18n.xaxistitletime : i18n.xaxistitledist;
            o.yaxis.title = _this.options.y == 'elev' ? i18n.yaxistitleelev : (_this.options.y == 'heartrate' ? i18n.yaxistitleheartrate : i18n.yaxistitlevel);
            o.xDecs = o.xDecs || decimals[o.x];
            o.yDecs = o.yDecs || decimals[o.y];
            o.xFactor = o.xFactor || factors[o.x];
            o.yFactor = o.yFactor || factors[o.y];
            o.minDeltaX = o.minDeltaX || delta[o.x];
            o.mouse.track = o.trackingEnabled;
            if (o.zoomEnabled) {
                o.selection.mode = 'xy';
            }
            else {
                o.selection.mode = null;
                Object.extend(o.xaxis, {min:null, max:null});
                Object.extend(o.yaxis, {min:null, max:null});
                this.container.fire('tools4gps:mapautozoom');
            }
        };
        
        // Formatter function to display values in the profile tracking area
        function _trackFormatter(obj) { 
            return labels[_this.options.x] + ' = ' +
                (_this.options.x == 'hour' ? _hourToString(obj.x) : _formatNumber(obj.x, _this.options.xDecs)) + 
                ' ' + labels[_this.options.y] + ' = ' +
                (_this.options.y == 'hour' ? _hourToString(obj.y) : _formatNumber(obj.y, _this.options.yDecs)); 
        }
        
        // Draw the graph with automatic min and max axis values
        function _clearEvent() {
            Object.extend(_this.options.xaxis, {min:null, max:null});
            Object.extend(_this.options.yaxis, {min:null, max:null});
            _this.draw();
            _this.container.fire('tools4gps:mapautozoom');
        }
        
        // Draw a new graph with bounded axis. The axis correspond to the selection just made.
        function _zoomEvent(evt) {
            var o = _this.options;
            var area = evt.memo[0];
            var x = _this.options.x;
            var y = _this.options.y;
            Object.extend(o.xaxis, (area.x2 - area.x1 > minzoom[x]) ? 
                {min:area.x1, max:area.x2} : 
                {min:(area.x1 + area.x2 - minzoom[x]) / 2, max: (area.x1 + area.x2 + minzoom[x]) / 2});
            Object.extend(o.yaxis, (area.y2 - area.y1 > minzoom[y]) ?
                {min:area.y1, max:area.y2} :
                {min:(area.y1 + area.y2 - minzoom[y]) / 2, max: (area.y1 + area.y2 + minzoom[y]) / 2});
            _this.draw();
        }

        /** 
        * Used by this.loadData to load tracks and routes
        */
        function _loadProfile(tp, o, topts) {
            var lastpoint = tp[0][0];
            var pc = (o.x.substr(0,4) == 'dist' ? o.colors[0] : o.colors[1]);
            var pointArray = {data:[], color: pc, lines: {show: true}};
            _setOptions(topts, pointArray);
            for (var i=0; i < tp.length; i++) {              // for each segment
                for (var j=0; j < tp[i].length; j++) {       // for each point
                    if ((i+j === 0) || (tp[i][j][o.x] - lastpoint[o.x] > o.minDeltaX)) {
                        pointArray.data.push([tp[i][j][o.x] * o.xFactor,  tp[i][j][o.y] * o.yFactor]);
                        lastpoint = tp[i][j];
                    }
                }
                if (!o.join) {
                    profileData.push(pointArray);
                    pointArray = Object.clone(pointArray);
                    pointArray.data = [];
                }
            }
            if (o.join) {
                profileData.push(pointArray);
            }
        }
        
        /**
         * Load selected profile properties from gpsdata. 
         * See x and y properties in {@link GPS.Profile#options}
         */
        this.loadData = function() {
            var o = _this.options;
            profileData = [];
            // load tracks
            if (o.trk && this.gpsData.numOfTrackpoints > 0) {
                _loadProfile(this.gpsData.trackpoints, o, o.trk);
            }
            // load routes
            if (o.rte && this.gpsData.numOfRoutes > 0) {
                _loadProfile(this.gpsData.routepoints, o, o.rte);
            }
            // load waypoints
            if (o.wpt && this.gpsData.numOfWaypoints) {
                   var pc = (o.x.substr(0,4) == 'dist' ? o.colors[0] : o.colors[1]);
                var waypoints = {data: [], color: pc, points: {show: true, fillColor: pc}};
                _setOptions(o.wpt, waypoints);
                var wp = this.gpsData.waypoints;
                for (var i=0; i < wp.length; i++) {
                    if (wp[i][o.x] !== null) {
                        waypoints.data.push([wp[i][o.x] * o.xFactor,  wp[i][o.y] * o.yFactor]);
                    }
                }
                profileData.push(waypoints);
            }
        };

        /**
         * Shows the profile.
         * @param {Object} opts User defined options 
         * @see GPS.Profile#options
         */
        this.draw = function(opts) {
            // Clone the options, so the 'options' variable always keeps intact, and adds user opts.
            var o = Object.clone(this.options);
            _setOptions(opts, o);
            o.wpt = null;
            o.trk = null;
            o.rte = null;
            Flotr.draw(this.container, profileData, o);
        };
        
        // Constructor
        // Array of segments wich are arrays of [x,y] pairs extracted from 
        // the selected properties in this.gpsData. See x and y properties in GPS.Profile#options
        var profileData = [];
        // Units labels
        var units = {dist: i18n.dist, dist3d: i18n.dist, hour: i18n.time, 
            elev: i18n.elev, vel: i18n.vel, vel3d: i18n.vel, heartrate: i18n.heartrate};
        // Labels for x-y values
        var labels = {dist: i18n.labeldist, dist3d: i18n.labeldist, hour: i18n.labeltime, 
            elev: i18n.labelelev, vel: i18n.labelvel, vel3d: i18n.labelvel, heartrate: i18n.labelheartrate};
        // Units conversion factors
        var factors = { dist: i18n.distFactor, dist3d: i18n.distFactor, hour: i18n.timeFactor, 
            elev: i18n.elevFactor, vel: i18n.velFactor, vel3dFactor: i18n.velFactor, heartrateFactor: i18n.heartrateFactor };
        // Fixed decimals for tracking values
        var decimals = {'dist': i18n.distDecs, 'dist3d': i18n.distDecs, 'elev': i18n.elevDecs, 
                'vel': i18n.velDecs, 'vel3d': i18n.velDecs, 'heartrate': i18n.heartrateDecs};
        // Minimun diference to draw a point.
        var delta = {dist: 0.02, dist3d: 0.02, hour: 20/3600, elev: 0, vel: 0, vel3d: 0, heartrate: 0};
        // Minimun allowed zoom
        var minzoom = {dist: 0.1, dist3d: 0.1, hour: 0.1, elev: 10, vel: 1, vel3d: 1, heartrate: 1};
        // Private scope access to this.
        var _this = this;
        this.setOptions(opts);
        this.container.observe('flotr:select', _zoomEvent);
        this.container.observe('dblclick', _clearEvent);
        this.loadData();
        this.draw();
    };
  
    /**
     * Create a GPS.Report object
     * <br>Examples: http://jstools4gps.javiersanp.com
     * @param {string} idprofile Id of the report DOM element
     * @param {GPS.Data} gpsData Source of data
     * @param {Object} opts User defined options. See {@link GPS.Report#options}
     * @class Class to show a report with global properties derived from a track
     * @see GPS.Summary
     */
    GPS.Report = function(idreport, gpsData, opts) {
        /** Id of the DOM node than contains the report. See {@link GPS.Report#draw}
         * @type node */
        this.container = $(idreport);
        /** 
         * User defined options. Default values: 
         * <pre>{
         *    decimals: {...}, // values from i18n. See {@link GPS.setI18N}
         *    factors: {...},  // values from i18n. See {@link GPS.setI18N}
         *    chart: false,
         * }</pre>
         * Properties:
         * <pre>{Object} decimals - a list of number of fixed decimals for each available property
         * {Object} factors - a list of units conversion factors for each available property
         * {GPS.Profile} chart - A Profile object to link with (recalculate summary and redraw
         *     on zoom event ) or false. 
         * You can use also any summary option (see {@link GPS.Summary#options})
         * @type Object
         */
        this.options = {
            decimals: {'totDist': i18n.distDecs, 'totDist3d': i18n.distDecs, 'maxElev': i18n.elevDecs, 
                'minElev': i18n.elevDecs, 'elevGain': i18n.elevDecs, 'elevLoss': i18n.elevDecs, 
                'avgVel': i18n.velDecs, 'maxVel': i18n.velDecs, 'maxHeartrate': i18n.heartrateDecs, 
                'minHeartrate': i18n.heartrateDecs, 'avgHeartrate': i18n.heartrateDecs},
            factors: {'totDist': i18n.distFactor, 'totDist3d': i18n.distFactor, 
                'maxElev': i18n.elevFactor, 'minElev': i18n.elevFactor, 'elevGain': i18n.elevFactor, 
                'elevLoss': i18n.elevFactor, 'avgVel': i18n.velFactor, 'maxVel': i18n.velFactor, 
                'maxHeartrate': i18n.heartrateFactor, 'minHeartrate': i18n.hearthrateFactor, 
                'avgHeartrate': i18n.heartrateFactor},
            chart: false
        };
        /** Summary calculated from gpsData
         * @type GPS.Summary */
        this.summary = new GPS.Summary(gpsData, opts);

        /**
         * Adds user values to default options replacing existing ones.
         * @param {Object} opts User options 
         * @see GPS.Map#options
         */
        this.setOptions = function(opts) {
            _setOptions(opts, this.options);
            this.summary.setOptions(opts);
        };

        /**
         * Dumps report values in the this.container child elements wich
         * class name coincide with any this.summary field name. 
         * See {@link GPS.Summary}. Previous innerHtml values are used as sufixs
         * @param {uint} from Initial index value of the range in this.data
         * @param {uint} to Last index value of the range in this.data
         */
        this.draw = function(from, to) {
            var g = this.summary;
            var decimals = this.options.decimals;
            g.calculate(from, to);
            for (var i in g) {
                var s = this.container.getElementsByClassName(i);
                for (var j=0; j < s.length; j++) {
                    var v = (this.options.factors[i] !== undefined) ? g[i] * this.options.factors[i] : g[i];
                    v = (i.substr(0, 7) == 'totTime') ? _hourToString(v) : _formatNumber(v, decimals[i] );
                    s[j].units = s[j].units || s[j].innerHTML;
                    s[j].innerHTML =  v + s[j].units;
                }                    
            }
        };
        
        // Constructor
        var _this = this;
        this.setOptions(opts);
        var chart = this.options.chart;
        if (chart) {
            // Hooks to the chart select event for report redrawing on zoom
            chart.container.observe('flotr:select', function(evt) {
                var area = evt.memo[0];
                var i = gpsData.indexOf(area.x1, _this.options.chart.options.x);
                var j = gpsData.indexOf(area.x2, _this.options.chart.options.x);
                _this.draw(i, j);
            });
        }
        this.draw();
        return this;
    };

    /**
     * Create a GPS.Planner object
     * <br>Examples: http://jstools4gps.javiersanp.com
     * @param {string} idptable Id of a HTML table element
     * @param {GPS.Data} gpsData Source of data
     * @param {Object} opts User defined options. See {@link GPS.Planner#options}
     * @class Class to show a table of selected points extracted from a source
     * (the waypoints, routes or tracks). It can calculate also a track GPS.Summary
     * object for each point.
     */
    GPS.Planner = function(idtable, gpsData, opts) {
        /** Id of the DOM node than contains the table. See {@link GPS.Planner#draw}
         * @type node */
        this.container = $(idtable);
        /** 
         * User defined options. Default values: 
         * <pre>{
         *     decimals: {lat: 6, lon: 6, lng: 6, ...}, // remainding values from i18n. See {@link GPS.setI18N}
         *     factors: {...}  // values from i18n. See {@link GPS.setI18N}
         *     source: 'wpt', 
         *     sourceRef: 1,
         *     includeStart: true,
         *     includeEnd: true
         * }</pre>
         * Properties:
         * <pre>{Object} decimals - A list of number of fixed decimals for each available property
         * {Object} factors - a list of units conversion factors for each available property
         * {string} source: 'trk', 'wpt' or 'rte'
         * {uint | [uint]} sourceref: When source is 'trk', it could be a number indicating a 
         *     fixed interval of Kilometers to extract points from the track, or an array of numbers 
         *     indicating the exact distances. When source is 'rte' or 'wpt', it could be a number
         *     indicating the step to extract points from the first one, or an array of indexs.
         * {boolean} includeStart: If true, forces to include first source point.
         * {boolean} includeEnd: If true, forces to include last source point.</pre>
         * You can use also any summary option (see {@link GPS.Summary#options})
         * @type Object
         */
        this.options = {
            decimals: {lat: 6, lon: 6, lng: 6, 'dist': i18n.distDecs, 'dist3d': i18n.distDecs, 'elev': i18n.elevDecs, 
                'vel': i18n.velDecs, 'vel3d': i18n.velDecs, 'heartrate': i18n.heartrateDecs,
                'totDist': i18n.distDecs, 'totDist3d': i18n.distDecs, 'maxElev': i18n.elevDecs, 
                'minElev': i18n.elevDecs, 'elevGain': i18n.elevDecs, 'elevLoss': i18n.elevDecs, 
                'avgVel': i18n.velDecs, 'maxVel': i18n.velDecs, 'maxHeartrate': i18n.heartrateDecs, 
                'minHeartrate': i18n.heartrateDecs, 'avgHeartrate': i18n.heartrateDecs},
            factors: {'dist': i18n.distFactor, 'dist3d': i18n.distFactor, 'hour': i18n.timeFactor,  
                'elev': i18n.elevFactor, 'vel': i18n.velFactor, 'vel3d': i18n.velFactor, 
                'totDist': i18n.distFactor, 'totDist3d': i18n.distFactor, 
                'maxElev': i18n.elevFactor, 'minElev': i18n.elevFactor, 'elevGain': i18n.elevFactor, 
                'elevLoss': i18n.elevFactor, 'avgVel': i18n.velFactor, 'maxVel': i18n.velFactor, 
                'maxHeartrate': i18n.heartrateFactor, 'minHeartrate': i18n.hearthrateFactor, 
                'avgHeartrate': i18n.heartrateFactor},
            source: 'wpt',
            sourceRef: 1,
            includeStart: true,
            includeEnd: true
        };
        
        /**
         * Adds user values to default options replacing existing ones.
         * @param {Object} opts User options 
         * @see GPS.Map#options
         */
        this.setOptions = function(opts) {
            _setOptions(opts, this.options);
        };

        /**
         * Extract the selected points to be showed in the table.
         * @see GPS.Map#options
         */
        this.loadData = function() {
            points = [];
            summaries = [];
            var r = this.options.sourceRef;
            var l = 0, i = 0, j = 0, k = 0;
            var o = this.options;
            function inck() {
                if (Object.isArray(r)) {
                    k = r[l];
                    l++;
                }
                else {
                    k += r;
                }
            }
            if (!o.includeStart || (o.includeStart && r[0] == 0)) {
                inck();
            }
            // if source is routes
            if (o.source.charAt(0).toUpperCase() == 'R') {
                var m = 0, last = Number.MAX_VALUE;
                for (i=0; i < gpsData.numOfRoutes; i++) {
                    for (j=0; j < gpsData.routepoints[i].length; j++) {
                        if (m == k) {
                            points.push(gpsData.routepoints[i][j]);
                            last = m;
                            inck();
                        }
                        m++;
                    }
                }
                if (o.includeEnd && (last + 1 < gpsData.numOfRoutepoints)) {
                    points.push(gpsData.routepoints[gpsData.numOfRoutepoints - 1]);
                }
            }
            // if source is waypoints
            else if (o.source.charAt(0).toUpperCase() == 'W') {
                var last = Number.MAX_VALUE;
                for (var i=0; i < gpsData.numOfWaypoints; i++) {
                    if (i == k) {
                        points.push(gpsData.waypoints[i]);
                        last = i;
                        inck();
                    }
                }
                if (o.includeEnd && (last + 1 < gpsData.numOfWaypoints)) {
                    points.push(gpsData.waypoints[gpsData.numOfWaypoints - 1]);
                }
            }
            // if source is tracks
            else {
                for (i=0; i < gpsData.numOfTrackpoints; i++) {
                    var p = gpsData.pointAt(i);
                    if (p.dist >= k) {
                        points.push(p);
                        inck();
                    }
                }
                if (o.includeEnd) {
                    points.push(gpsData.pointAt(gpsData.numOfTrackpoints - 1));
                }
            }
            _loadSummaries();
        };

        /**
          * If this.container table contains a th column with a id refered to a GPS.Summary property, 
          * calculates a Summary for each point.  It needs loadData to be loaded first.
          */
        function _loadSummaries() {
            // Determines if the head of the table contains any Summary property tag.
            var summarytags = "#totdist#totdist3d#minelev#maxelev#elevgain#elevloss#maxvel" +
                    "#avgvel#tottime#tottimestop#tottimeup#tottimedown#tottimeflat#totTimeMove" + 
                    "#minheartrate#maxheartrate#avgheartrate#";
            var head = _this.container.getElementsByTagName('th');
            var i = 0;
            for (i=0; i < head.length; i++) {
                if (summarytags.search("#"+head[i].className.toLowerCase()+"#") > 0) {
                    hasSummaries = true;
                    break;
                }
            }
            if (hasSummaries) {
                var j = 0;
                for (i=0; i < points.length; i++) {
                    summaries.push(new GPS.Summary(gpsData, _this.options));
                    var src = summaries[i].options.source;
                    var from = (src == 'rte' ? j : gpsData.indexOf(points[j].dist, 'dist'));
                    var to = (src == 'rte' ? i : gpsData.indexOf(points[i].dist, 'dist'));
                    summaries[i].calculate(from, to);
                    j = i;
                }
            }
        };
        
        /**
         * Dumps points values in the table this.container. The table must have
         * at least a row with a &lt;th&gt; element for each desired point property 
         * identified by the class name. See {@link GPS.Point}. There is also available 
         * the 'num' property for the correlative order of the point. Existing tbody rows are
         * used as a model for the next ones that would be needed to fill the table.<br/>
         */
        this.draw = function() {
            var c = this.container;
            // Restores original table
            if (c.backup === undefined) {
                c.backup = c.tBodies[0].cloneNode(true);
            } else {
                c.removeChild(c.tBodies[0]);
                c.appendChild(c.backup.cloneNode(true));
            }
            // get column names and void row model
            var head = c.getElementsByTagName('th');
            var tBody = c.tBodies[0];
            var fields = [];
            var i = 0;
            var lrow = document.createElement('tr');
            for (i=0; i < head.length; i++) {
                fields.push(head[i].className);
            }
            var decimals = this.options.decimals;
            var rows = c.tBodies[0].rows;
            var inrows = rows.length;
            var k = 0;
            // for each point
            for (i=0; i < points.length; i++) {
                var point = points[i];
                // if there isn't enough rows inserts one.
                var row = (i < rows.length) ? rows[i] : tBody.appendChild(lrow);
                var cells = row.cells;
                for (var j=0; j < head.length; j++) { // for each cell
                    // if there isn't enough cells  inserts one.
                    if (cells.length < head.length) {
                        row.insertCell(-1);
                    }
                    var field = fields[j];
                    var val = null;
                    if (field == "num") {
                        val = i+1;
                    }
                    else if (point[field] !== undefined) {
                        val = point[field];
                    }
                    else if (hasSummaries) {
                        var summary = summaries[i];
                        val = (summary[field] !== undefined) ? summary[field] : null;
                    }
                    head[j].units = (head[j].units === undefined) ? cells[j].innerHTML : head[j].units;
                    cells[j].innerHTML = "";
                    val = (this.options.factors[field] !== undefined) ? val * this.options.factors[field] : val;
                    if (val !== null) {
                        if (field == 'hour' || field.substr(0, 7) == 'totTime') {
                            val = _hourToString(val)
                        }
                        else if ((field == 'time') && (val.format !== undefined)) {
                            val = val.format(i18n.dateTimeFormat);
                        }
                        else {
                            val = _formatNumber(val, decimals[field]);
                        }
                        cells[j].innerHTML = val + head[j].units;
                    }
                }
                if (i+1 >= inrows) {
                    lrow = tBody.rows[k].cloneNode(true);  // use this row as model for the next
                    k++;
                }
            }
        };
        
        // Constructor
        var _this = this;
        var points = [];
        var summaries = [];
        var hasSummaries = false;
        this.setOptions(opts);
        this.loadData();
        this.draw();
        return this;
    };

    /** 
     * Create a GPS.Button object.
     * <br>Examples: http://jstools4gps.javiersanp.com
     * @class Class to control a button.
     */
    GPS.Button = function(container, opts) {
        /** DOM node than contains the button. See {@link GPS.Button#draw}
         * @type node */
        this.container = Element.extend(container);
        /** 
         * User defined options. Properties:
         * <pre>{[string|boolean]} values - This array defines the behavoir of the button. 
         *     There are three kinds:
         *     - One click. The default image (its src file name ends with '_off') is 
         *     alternated with a highlitted one (the image ends with '_on') when the mouse is over. 
         *     It regret to normal state on mouse out. To use it omit the 'values' property.
         *     - Multi-state. Rotates between each value in values when clicked. The 
         *     button image for each state must end with 'value_off'. They are highlitted
         *     also (changes between '_off' / '_on') on mouse over. Example axis  buttons.
         *     - On/off. After clicked, the button keeps its highlitted state when mouse 
         *     is out until next click. Use values [false, true] or [true, false] for this. 
         *     Example: zoom button.
         * {function} onClick - Function that will be called on click event.
         * {string} tag - String to be used in the tag and title attributes of the image
         *     for one click buttons. For multi-state or on/off, use a proporty with the
         *     name 'tagvalue' for each value. If you don't set them, the i18n settings 
         *     will be used. See {@link GPS.setI18N}</pre>
         * @type Object
         */
        this.options = {
            values: [""],
            onClick: function() {},
            tag: ""
        }

        /**
         * Adds user values to default options replacing existing ones.
         * @param {Object} opts User options 
         * @see GPS.ButtonBar#options
         */
        this.setOptions = function(opts) {
            var o = this.options;
            _setOptions(opts, o);
            for (var i = 0; i < o.values.length; i++) {
                o['tag' + o.values[i]] = o['tag' + o.values[i]] || 
                    i18n["tagbutton" + this.container.className + o.values[i]];
            }
        };        

        /** Highlights this button
         * @type function */
        this.setOn = function() {
            _this.container.src = _this.container.src.replace( /_off(\..*)*/, '_on$1');
        }

        /** Revert to normal state a button
         * @type function */
        this.setOff = function() {
            if (_this.options.values[_activeval] !== true) {
                _this.container.src = _this.container.src.replace(/_on(\..*)*/, '_off$1');
            }
        }

        /** Return the active state value
         * @type function */
        this.getActiveValue = function() {
            return this.options.values[_activeval];
        }
        
        /** Return the value following the active state in the values array.
         * @type function */
        this.getNextValue = function() {
            return this.options.values[_nextval()];
        }
        
        /**
         * Sets title and click event of button contained in this.container and
         * defined like a &lt;img&gt; element. See {@link GPS.Button#options})
         */
        this.draw = function() {
            this.container.title = this.options['tag' + this.options.values[0]];
            this.container.alt = this.container.title;
            this.container.observe('click', function(evt) {
                _this.options.onClick(_this);
                var v = _this.getActiveValue(), w = _this.getNextValue();
                if (_this.options.values.length > 1) {
                    if (_typeOf(v) == "boolean") {
                        _this['set' + (v ? 'Off' : 'On')]();
                    }
                    else {
                        _this.container.src = _this.container.src.replace(v+"_", w+"_");
                    }
                    _activeval = _nextval();
                }
                _this.container.title = _this.options['tag' + w];
                _this.container.alt = _this.container.title;
            });
        }

        // Constructor. 
        var _this = this;
        var _activeval = 0;    
        var _nextval = function() {
            return (_activeval + 1 == _this.options.values.length) ? 0 : _activeval + 1;
        }
        this.setOptions(opts);
        this.container.observe('mouseover', this.setOn);
        this.container.observe('mouseout', this.setOff);
        this.draw();
        return this;
    };
    
    /**
     * Create a GPS.ButtonBar object
     * @param {string} idbuttons Id of the button bar DOM element
     * @param {Object} opts User defined options. See {@link GPS.ButtonBar#options}
     * @class Class to control a button bar
     */
    GPS.ButtonBar = function(idbuttons, opts) {
        /** Id of the DOM node than contains the buttonbar. See {@link GPS.ButtonBar#draw}
         * @type node */
        this.container = $(idbuttons);
        /** 
         * User defined options.<br/> Default values: 
         * <pre>{
         *    zoom: {values: [false, true],
         *           onClick: // Enable or disable the profile zoom 
         *    }, 
         *    xaxis: {values: ['dist', 'hour'],
         *            onClick: // Change the data showed in x axis
         *    }, 
         *    yaxis: {values: ['elev', 'vel'],
         *            onClick: // Change the data showed in y axis
         *    }
         * }</pre>
         * Properties:
         * <pre{GPS.Profile} chart - A GPS.Profile object target of the buttons actions
         * {GPS.Report} report - A GPS.Report object target of the zoom button action
         * {GPS.Button} buttonId1
         *   ...
         * {GPS.Button} buttonIdN - You can define as many buttons as needed, either 
         * as a GPS.Button instance or in JSON format. 'buttonId' is ussed to locate 
         * the button and must coincide with the class of its &lt;img&gt; element.</pre>
         * See {@link GPS.Button}
         */
        this.options = {
            zoom: {  // zoom tool
                values: [false, true],
                /** @private */
                onClick: function(button) {
                    chart.setOptions({zoomEnabled: !button.getActiveValue()});
                    if (button.getActiveValue() && report) {
                        report.draw();
                    }
                    chart.draw();
                }
            }, 
            xaxis: {  // change x axis data
                values: ['dist', 'hour'],
                /** @private */
                onClick: function(button) {
                    chart.setOptions({x: button.getNextValue()});
                    chart.loadData();
                    if (report && chart.options.zoomEnabled) {
                        report.draw();
                    }
                    chart.draw();
                }
            }, 
            yaxis: {  // change y axis data
                values: ['elev', 'vel'],
                /** @private */
                onClick: function(button) {
                    chart.setOptions({y: button.getNextValue()});
                    chart.loadData();
                    if (report && chart.options.zoomEnabled) {
                        report.draw();
                    }
                    chart.draw();
                }
            },
            chart: null, report: null
        };
        
        /**
         * Adds user values to default options replacing existing ones.
         * @param {Object} opts User options 
         * @see GPS.ButtonBar#options
         */
        this.setOptions = function(opts) {
            _setOptions(opts, this.options);
        };        

        /**
         * Instantiates in this options or draw each button identified by the
         * class atribute of any img element in the container.
         * @see GPS.ButtonBar#options
         */
        this.draw = function() {
            var s = this.container.getElementsByTagName('img');
            for (var i=0; i < s.length; i++) {
                var id = s[i].className;
                if (this.options[id]) {
                    if (this.options[id] instanceof GPS.Button) {
                        this.options[id].draw();
                    }
                    else {
                        this.options[id] = new GPS.Button(s[i], this.options[id]);
                    }
                }
            }
        }

        // Constructor. 
        this.setOptions(opts);
        var chart = this.options.chart, report = this.options.report;
        this.draw();
        return this;
    };
    
    return GPS;

} ();
